/*
 * Copyright 2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.easipass.framework.eirframeworkcoredsl;

import com.easipass.framework.eirframeworkcoredsl.graph.Graph;
import com.easipass.framework.eirframeworkcoredsl.graph.Link;
import com.easipass.framework.eirframeworkcoredsl.graph.Node;

import java.util.*;

public class GraphGeneratorVisitor extends AppValidatorVisitor {

    private int nextNodeId = 0;

    private Stack<Context> contexts = new Stack<>();

    private List<Sequence> sequences = new ArrayList<>();

    private int currentSequence;

    private String currentTaskAppId;

    private Map<String, Node> existingNodesToReuse;

    public Graph getGraph() {
        if (sequences.size() == 0) {
            List<Node> nodes = new ArrayList<>();
            List<Link> links = new ArrayList<>();
            nodes.add(new Node("0", "START", Node.START));
            nodes.add(new Node("1", "END", Node.END));
            links.add(new Link("0", "1"));
            return new Graph(nodes, links);
        } else {
            List<Node> nodes = new ArrayList<>();
            List<Link> links = new ArrayList<>();
            for (Sequence s : sequences) {
                nodes.addAll(s.nodes);
                links.addAll(s.links);
            }
            Graph g = new Graph(nodes, links);
            return g;
        }
    }

    private String nextId() {
        return Integer.toString(nextNodeId++);
    }

    /*@Override
    public boolean preVisitSequence(LabelledServiceNode firstNode) {
        Node sequenceStartNode = new Node(nextId(), "START", Node.START);
        currentSequence = sequenceNumber;
        sequences.add(new Sequence(sequenceNumber, firstNode.getLabelString(), sequenceStartNode));
        contexts.push(new Context(true, false, false, sequenceStartNode.id, null));
        return true;
    }*/

    @Override
    public void postVisit(SourceDestinationNode source) {
        Node sequenceStartNode = new Node(nextId(), source.getDestinationName(), Node.START);
        currentSequence = sequenceNumber;
        sequences.add(new Sequence(sequenceNumber, source.getDestinationName(), sequenceStartNode));
        contexts.push(new Context(false, false, false, sequenceStartNode.id, null));
    }

    @Override
    public void postVisit(SinkDestinationNode sink) {
        String endId = nextId();
        Node endNode = new Node(endId, sink.getDestinationName(), Node.END);
        addLinks(endId);
        addNode(endNode);
        contexts.pop();
        sequenceNumber++;
    }


    /* @Override
    public void postVisitSequence(LabelledServiceNode firstNode) {
        String endId = nextId();
        Node endNode = new Node(endId, "END", Node.END);
        addLinks(endId);
        addNode(endNode);
        contexts.pop();
        sequenceNumber++;
    }*/

    private void addLink(Link link) {
        sequences.get(currentSequence).links.add(link);
    }

    private void addNode(Node node) {
        sequences.get(currentSequence).nodes.add(node);
    }

    private void addLinks(String target) {
        List<String> openNodes = currentContext().getDanglingNodes();
        for (int i = 0; i < openNodes.size(); i++) {
            addLink(new Link(openNodes.get(i), target));
        }
    }

    @Override
    public void endVisit() {
        if (sequences.size() > 0) {
            Sequence mainSequence = sequences.get(0);
            int tooMany = 0;
            while (!mainSequence.outstandingTransitions.isEmpty() && tooMany < 50) {
                List<Context.TransitionTarget> nextTransitions = findNextTransitions(
                        mainSequence.outstandingTransitions);
                mainSequence.outstandingTransitions.removeAll(nextTransitions);
                Sequence sequence = findSequence(nextTransitions.get(0).label);
                if (sequence == null) {
                    throw new IllegalStateException("Out of flow transition? " + nextTransitions.get(0));
                }
                inline(mainSequence, sequence, nextTransitions);
                Iterator<Context.TransitionTarget> iter = mainSequence.outstandingTransitions.iterator();
                while (iter.hasNext()) {
                    Context.TransitionTarget transitionTarget = iter.next();
                    FlowNode flowInWhichTransitionOccurring = transitionTarget.flow;
                    Map<String, Node> candidates = mainSequence.labeledNodesInEachFlow
                            .get(flowInWhichTransitionOccurring);
                    for (Map.Entry<String, Node> candidate : candidates.entrySet()) {
                        if (candidate.getKey().equals(transitionTarget.label)) {
                            // This is the right one!
                            mainSequence.links.add(new Link(transitionTarget.nodeId, candidate.getValue().id,
                                    transitionTarget.onState));
                            iter.remove();
                        }
                    }
                }
                tooMany++;
            }
        }
    }

    private List<Context.TransitionTarget> findNextTransitions(List<Context.TransitionTarget> transitions) {
        if (transitions.size() == 0) {
            return Collections.emptyList();
        }
        List<Context.TransitionTarget> sameTarget = new ArrayList<>();
        sameTarget.add(transitions.get(0));
        for (int i = 1; i < transitions.size(); i++) {
            Context.TransitionTarget tt = transitions.get(i);
            if (transitions.get(0).flow.equals(tt.flow) && transitions.get(0).label.equals(tt.label)) {
                sameTarget.add(tt);
            }
        }
        return sameTarget;
    }

    private Sequence findSequence(String label) {
        for (Sequence sequence : sequences) {
            if (label.equals(sequence.label)) {
                return sequence;
            }
            for (FlowNode flowNode : sequence.labeledNodesInEachFlow.keySet()) {
                if (label.equals(flowNode.getLabelString())) {
                    return sequence;
                }
            }
        }
        return null;
    }

    private void inline(Sequence mainSequence, Sequence sequence,
                        List<Context.TransitionTarget> transitionTargets) {

        Map<String, String> nodeIds = new HashMap<>();

        Node startNode = sequence.nodes.get(0);
        Node endNode = sequence.nodes.get(sequence.nodes.size() - 1);
        for (int i = 1; i < (sequence.nodes.size() - 1); i++) {
            Node n = sequence.nodes.get(i);
            Node newNode = new Node(nextId(), n.name, Node.FREEMOD, n.properties);
            nodeIds.put(n.id, newNode.id);
            mainSequence.nodes.add(newNode);
        }

        for (int i = 0; i < sequence.links.size(); i++) {
            Link existingLink = sequence.links.get(i);
            String existingLinkFrom = existingLink.from;
            String existingLinkTo = existingLink.to;
            Link newLink = null;
            if (existingLinkFrom.equals(startNode.id)) {
                for (Context.TransitionTarget tt : transitionTargets) {
                    newLink = new Link(tt.nodeId, nodeIds.get(existingLinkTo), tt.onState);
                    mainSequence.links.add(newLink);
                }
            } else if (existingLinkTo.equals(endNode.id)) {
                Context.TransitionTarget tt = transitionTargets.get(0);
                String finalNodeInMainSequence = tt.lastNodeId;
                List<Link> newLinks = new ArrayList<>();
                for (Link l : mainSequence.links) {
                    if (l.from.equals(finalNodeInMainSequence)) {
                        newLinks.add(new Link(nodeIds.get(existingLinkFrom), l.to, l.getTransitionName()));
                    }
                }
                mainSequence.links.addAll(newLinks);
            } else {
                newLink = new Link(nodeIds.get(existingLinkFrom), nodeIds.get(existingLinkTo),
                        existingLink.getTransitionName());
                mainSequence.links.add(newLink);
            }
        }
        List<Context.TransitionTarget> rewrittenTransitions = new ArrayList<>();
        for (Context.TransitionTarget looseEnd : sequence.outstandingTransitions) {
            Context.TransitionTarget tt = new Context.TransitionTarget(nodeIds.get(looseEnd.nodeId), looseEnd.onState,
                    looseEnd.label);
            tt.flow = transitionTargets.get(0).flow;
            tt.lastNodeId = transitionTargets.get(0).lastNodeId;// nodeIds.get(looseEnd.lastNodeId);
            rewrittenTransitions.add(tt);
        }
        mainSequence.outstandingTransitions.addAll(rewrittenTransitions);
        FlowNode flowBeingInsertedInto = transitionTargets.get(0).flow;
        Map<String, Node> relevantFlowMapToUpdate = mainSequence.labeledNodesInEachFlow.get(flowBeingInsertedInto);
        Map<FlowNode, Map<String, Node>> labeledNodesInSequenceBeingInlined = sequence.labeledNodesInEachFlow;
        FlowNode primaryFlowInSequenceBeingInlined = sequence.primaryFlow;
        for (Map.Entry<FlowNode, Map<String, Node>> entry : labeledNodesInSequenceBeingInlined.entrySet()) {
            if (entry.getKey() == primaryFlowInSequenceBeingInlined) {
                for (Map.Entry<String, Node> entry2 : entry.getValue().entrySet()) {
                    Node n2 = new Node(nodeIds.get(entry2.getKey()), entry2.getValue().name, Node.FREEMOD);
                    n2.setLabel(entry2.getValue().getLabel());
                    relevantFlowMapToUpdate.put(entry2.getKey(), n2);
                }
            } else {
                Map<String, Node> newMap = new HashMap<>();
                for (Map.Entry<String, Node> entry2 : entry.getValue().entrySet()) {
                    Node n2 = new Node(nodeIds.get(entry2.getKey()), entry2.getValue().name, Node.FREEMOD);
                    n2.setLabel(entry2.getValue().getLabel());
                    newMap.put(entry2.getKey(), n2);
                }
                mainSequence.labeledNodesInEachFlow.put(entry.getKey(), newMap);
            }
        }
    }

    @Override
    public boolean preVisit(SplitNode split) {
        List<String> open = currentContext().getDanglingNodes();
        String startId = (open.size() == 0 ? currentContext().startNodeId : open.get(0));
        if (open.size() > 1) {
            String syncId = nextId();
            Node node = new Node(syncId, "SYNC", Node.FREEMOD);
            addNode(node);
            for (String openid : open) {
                addLink(new Link(openid, syncId));
            }
            startId = syncId;
        }
        contexts.push(new Context(false, false, true, startId, split));
        return true;
    }

    @Override
    public void postVisit(SplitNode split) {
        List<String> openAtEndOfFlow = currentContext().getDanglingNodes();
        contexts.pop();
        currentContext().addDanglingNodes(true, openAtEndOfFlow);
    }

    @Override
    public boolean preVisit(FlowNode flow) {
        contexts.push(new Context(false, true, false, currentContext().startNodeId, flow));
        currentSequence().primaryFlow = flow;
        return true;
    }

    @Override
    public void postVisit(FlowNode flow) {
        List<Context.TransitionTarget> transitionTargets = currentContext().getTransitionTargets();
        for (Context.TransitionTarget tt : transitionTargets) {
            tt.lastNodeId = currentContext().getDanglingNodes().get(0);
            tt.flow = flow;
        }
        sequences.get(currentSequence).outstandingTransitions.addAll(transitionTargets);
        currentSequence().labeledNodesInEachFlow.put(flow, currentContext().nodesWithLabels);
        List<String> openAtEndOfFlow = currentContext().getDanglingNodes();
        List<String> otherExitNodes = currentContext().otherExits;
        contexts.pop();
        currentContext().addDanglingNodes(false, openAtEndOfFlow);
        currentContext().addDanglingNodes(false, otherExitNodes);
    }

    @Override
    public boolean preVisit(FunctionNode functionNode) {
        contexts.push(new Context(true, false, false, currentContext().startNodeId, functionNode));
        currentSequence().labeledFunction = functionNode;
        return true;
    }

    private Node findOrMakeNode(String name) {
        Node node = existingNodesToReuse.get(name);
        if (node == null) {
            node = new Node(nextId(), name, Node.FREEMOD);
            existingNodesToReuse.put(name, node);
            addNode(node);
        }
        return node;
    }

    @Override
    public void visit(TransitionNode transition) {
        if (transition.isTargetService()) {
            if (transition.isSpecialTransition()) {
                if (transition.isFailTransition()) {
                    Node failNode = findOrMakeNode("$FAIL");
                    addLink(new Link(currentTaskAppId, failNode.id, transition.getStatusToCheck()));
                } else if (transition.isEndTransition()) {
                    Node endNode = findOrMakeNode("$END");
                    addLink(new Link(currentTaskAppId, endNode.id, transition.getStatusToCheck()));
                }
            } else {
                String key = toKey(transition.getTargetService());
                Node n = existingNodesToReuse.get(key);
                boolean isCreated = false;
                if (n == null) {
                    String nextId = nextId();
                    n = new Node(nextId, transition.getTargetService().getName(), Node.FREEMOD,
                            toMap(transition.getTargetService().getArguments()));
                    if (transition.getTargetService().hasLabel()) {
                        n.setLabel(transition.getTargetService().getLabelString());
                    }
                    existingNodesToReuse.put(key, n);
                    addNode(n);
                    isCreated = true;
                }
                addLink(new Link(currentTaskAppId, n.id, transition.getStatusToCheck()));
                if (isCreated) {
                    if (currentContext().isFlow) {
                        currentContext().addOtherExit(n.id);
                    } else {
                        currentContext().addDanglingNodes(false, n.id);
                    }
                }
            }
        } else {
            Node existingNode = currentContext().getNodeLabeled(transition.getTargetLabel());
            if (existingNode != null) {
                addLink(new Link(currentTaskAppId, existingNode.id, transition.getStatusToCheck()));
            } else {
                currentContext().addTransitionTarget(currentTaskAppId, transition.getStatusToCheck(),
                        transition.getTargetLabel());
            }
        }
    }

    private Map<String, String> toMap(ArgumentNode[] arguments) {
        if (arguments == null) {
            return null;
        }
        Map<String, String> argumentsMap = new HashMap<>();
        for (ArgumentNode argument : arguments) {
            argumentsMap.put(argument.getName(), argument.getValue());
        }
        return argumentsMap;
    }

    private String toKey(ServiceNode targetApp) {
        StringBuilder key = new StringBuilder();
        if (targetApp.hasLabel()) {
            key.append(targetApp.getLabel()).append(">");
        }
        key.append(targetApp.getName());
        for (Map.Entry<String, String> argument : targetApp.getArgumentsAsMap().entrySet()) {
            key.append(":").append(argument.getKey()).append("=").append(argument.getValue());
        }
        return key.toString();
    }

    public Context currentContext() {
        return contexts.peek();
    }

    public Context parentContext() {
        if (contexts.size() < 2) {
            return null;
        } else {
            return contexts.get(contexts.size() - 2);
        }
    }

    public Sequence currentSequence() {
        return sequences.get(currentSequence);
    }

    @Override
    public void visit(ServiceNode serviceNode) {
        String nextId = nextId();
        currentTaskAppId = nextId;
        Node node = new Node(nextId, serviceNode.getName(), Node.FREEMOD, toMap(serviceNode.getArguments()));
        addNode(node);
        if (serviceNode.hasLabel()) {
            node.setLabel(serviceNode.getLabelString());
            currentContext().addNodeWithLabel(serviceNode.getLabelString(), node);
        }
        if (currentContext().isFunction) {
            if (serviceNode.hasLabel()) {
                for (Iterator<Context.TransitionTarget> iterator = currentContext().getTransitionTargets()
                        .iterator(); iterator.hasNext(); ) {
                    Context.TransitionTarget tt = iterator.next();
                    if (tt.label.equals(serviceNode.getLabelString())) {
                        addLink(new Link(tt.nodeId, nextId, tt.onState));
                        iterator.remove();
                    }
                }
            }
            List<String> danglingNodes = currentContext().getDanglingNodes();
            if (danglingNodes.size() == 0) {
                addLink(new Link(currentContext().startNodeId, nextId));
            } else {
                for (int i = 0; i < danglingNodes.size(); i++) {
                    addLink(new Link(danglingNodes.get(i), nextId));
                }
            }
            currentContext().addDanglingNodes(true, nextId);
        } else if (currentContext().isFlow) {
            if (serviceNode.hasLabel()) {
                for (Iterator<Context.TransitionTarget> iterator = currentContext().getTransitionTargets()
                        .iterator(); iterator.hasNext(); ) {
                    Context.TransitionTarget tt = iterator.next();
                    if (tt.label.equals(serviceNode.getLabelString())) {
                        addLink(new Link(tt.nodeId, nextId, tt.onState));
                        iterator.remove();
                    }
                }
            }
            List<String> danglingNodes = currentContext().getDanglingNodes();
            if (danglingNodes.size() == 0) {
                addLink(new Link(currentContext().startNodeId, nextId));
            } else {
                for (int i = 0; i < danglingNodes.size(); i++) {
                    addLink(new Link(danglingNodes.get(i), nextId));
                }
            }
            currentContext().addDanglingNodes(true, nextId);
        } else if (currentContext().isSplit) {
            if (currentContext().containingNode.hasLabel() && (parentContext() != null && parentContext().isFlow)) {
            }

            addLink(new Link(currentContext().startNodeId, nextId));
            currentContext().addDanglingNodes(false, nextId);
        }
        existingNodesToReuse = (currentContext().isFlow ? currentContext().extraNodes : new HashMap<>());
    }

    static class Sequence {

        final int sequenceNumber;

        final String label;

        final List<Node> nodes = new ArrayList<>();

        final List<Link> links = new ArrayList<>();

        final List<Context.TransitionTarget> outstandingTransitions = new ArrayList<>();

        final Map<FlowNode, Map<String, Node>> labeledNodesInEachFlow = new HashMap<>();

        FunctionNode labeledFunction;

        FlowNode primaryFlow;

        Sequence(int sequenceNumber, String label, Node sequenceStartNode) {
            this.sequenceNumber = sequenceNumber;
            this.label = label;
            nodes.add(sequenceStartNode);
        }

        public String toString() {
            StringBuilder s = new StringBuilder();
            for (Node n : nodes) {
                s.append("[").append(n.id).append(":").append(n.name).append("]");
            }
            for (Link l : links) {
                s.append("[" + (l.getTransitionName() == null ? "" : l.getTransitionName() + ":") + l.from + "-" + l.to
                        + "]");
            }
            s.append("  transitions:").append(outstandingTransitions);
            s.append("   flowLabelsMap:").append(labeledNodesInEachFlow);
            s.append("     functionLabels").append(labeledFunction);
            return s.toString();
        }
    }

    static class Context {

        final Map<String, Node> nodesWithLabels = new HashMap<>();

        public List<String> otherExits = new ArrayList<>();

        public List<TransitionTarget> transitionTargets = new ArrayList<>();

        public Map<String, Node> extraNodes = new HashMap<>();

        Map<String, Node> nodesSharedInFlow = new LinkedHashMap<String, Node>();

        LabelledServiceNode containingNode;

        boolean isFlow = false;

        boolean isSplit = false;

        boolean isFunction = false;

        private String startNodeId;

        private List<String> currentDanglingNodes = new ArrayList<>();

        Context(boolean isFunction, boolean isFlow, boolean isSplit, String startNodeId, LabelledServiceNode split) {
            this.isFunction = isFunction;
            this.isFlow = isFlow;
            this.isSplit = isSplit;
            this.startNodeId = startNodeId;
            this.containingNode = split;
        }

        public void addDanglingNodes(boolean replaceExisting, String... is) {
            if (replaceExisting) {
                currentDanglingNodes.clear();
            }
            for (String i : is) {
                currentDanglingNodes.add(i);
            }
        }

        public void addDanglingNodes(boolean replaceExisting, List<String> newDanglingNodes) {
            if (replaceExisting) {
                currentDanglingNodes.clear();
            }
            currentDanglingNodes.addAll(newDanglingNodes);
        }

        public List<String> getDanglingNodes() {
            return currentDanglingNodes;
        }

        public void clearDanglingNodes() {
            currentDanglingNodes.clear();
        }

        public void addTransitionTarget(String fromNodeId, String fromOnState, String targetLabel) {
            TransitionTarget tt = new TransitionTarget(fromNodeId, fromOnState, targetLabel);
            transitionTargets.add(tt);
        }

        public List<TransitionTarget> getTransitionTargets() {
            return this.transitionTargets;
        }

        public void addOtherExit(String nextId) {
            otherExits.add(nextId);
        }

        public Node getNodeLabeled(String label) {
            return nodesWithLabels.get(label);
        }

        public void addNodeWithLabel(String label, Node node) {
            nodesWithLabels.put(label, node);
        }

        static class TransitionTarget {
            String nodeId;

            String onState;

            String label;

            String lastNodeId;

            FlowNode flow;

            TransitionTarget(String fromNodeId, String fromOnState, String targetLabel) {
                this.nodeId = fromNodeId;
                this.onState = fromOnState;
                this.label = targetLabel;
            }

            public String toString() {
                StringBuilder s = new StringBuilder();
                s.append(nodeId).append(":").append(onState).append("->").append(label);
                return s.toString();
            }
        }
    }
}
